﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using DelegateToExpression;
using Entropy.Proxy;

namespace Entropy.Ext
{

	public enum AstExpressionType
	{
		Constructor = 1000,
		ConstructorParameter,
	    RootFactoryAccess
	}

    public class InterfacedFactory : IInterfacedFactoryAccess
	{
		private readonly ISyntaxTree builder;
		private readonly Func<string> onPublish;
        private readonly Type type;

        private InterfacedFactory(ISyntaxTree builder, Func<string> onPublish, Type type)
	    {
		    this.builder = builder;
		    this.onPublish = onPublish;
            this.type = type;
	    }

		internal static object Create(Type type, ISyntaxTree builder, Func<string> onPublish, List<CallArgument> args)
		{
			var proxy = new InterfacedFactory(builder, onPublish, type);
			OnInterfacedFactory(builder, type, args, proxy);
			return new RealProxyBasedFactory().CreateProxy(type, proxy.OnInvocation);
		}

		private IMethodReturnMessage OnInvocation(IMethodCallMessage msg, RemotingInvocation target)
		{
			if (msg.MethodName == "Access")
				return new ReturnMessage(this, new object[0], 0, null, msg);

			if (msg.MethodName == "GetType")
				return new ReturnMessage(type, new object[0], 0, null, msg);

			if (msg.MethodName == "ToHtmlString")
			{
				return new ReturnMessage(onPublish(), new object[0], 0, null, msg);
			}

			var declaringType = msg.MethodBase.DeclaringType;
		    Debug.Assert(declaringType != null);
		    if (declaringType.IsInterface)
	        {
				var methodInfo = (MethodInfo)msg.MethodBase;

		        if (msg.MethodName.StartsWith("set_"))
			        PropSet(methodInfo, msg.Args[0]);
		        else if (msg.MethodName.StartsWith("get_"))
			        PropGet(methodInfo);
		        else
			        Call(msg.MethodBase, msg.Args);

		        return new ReturnMessage(this, new object[0], 0, null, msg);
	        }

	        throw new Exception("Unexpected: " + msg.MethodName);
		}

		private void Call(MethodBase method, IEnumerable<object> args)
		{
			builder.Append(
				Expression.Call(
					Me,
					(MethodInfo)method,
					args.Select(arg => ResolveArgument(arg, builder))));
		}

        internal AstConstructor Me
		{
			get { return builder.InterfaceFactories[this]; }
		}

		private void PropSet(MethodInfo prop, object value)
		{
		    var arg = ResolveArgument(value, builder);
		    builder.Append(Expression.Assign(Expression.Property(Me, prop), arg));
		}

        private void PropGet(MethodInfo prop)
		{
			builder.Append(Expression.Property(Me, prop)); 
		}

		private static void OnInterfacedFactory(ISyntaxTree builder, Type type, IEnumerable<CallArgument> args, InterfacedFactory proxy)
		{
			var value = new AstConstructor(type)
			{
				Args = args.Select(arg => new AstConstructorParameter
				{
					Name = arg.ParamName,
					Value = ResolveArgument(arg.Value, builder),
					AsJson = arg.AsJson 
				}).ToArray()

			};
			builder.InterfaceFactories.Add(proxy, value);
			builder.Append(value);
		}

	    internal static bool IsConstant(object arg)
	    {
		    return (arg == null || arg is bool || arg is Enum || arg is int || arg is decimal || arg is double || arg is string);
	    }


	    private static Expression ResolveArgument(object arg, ISyntaxTree builder)
		{
			var interfacedFactory = arg as IInterfacedFactoryAccess;
			if (interfacedFactory != null)
			{
				return builder.InterfaceFactories[(InterfacedFactory) interfacedFactory.Access()];
			}

			var provides = arg as IProvidesValue;
            if (provides != null)
                arg = provides.Value;

		    if (IsConstant(arg)) 
				return Expression.Constant(arg);

		    var list = arg as IList<object>;
			if (list != null)
				return Expression.NewArrayInit(typeof(object), list.Select(o => ResolveArgument(o, builder)).ToArray());

		    var exp = arg as Expression;
			if (exp != null)
			{
				exp = new ExpressionVarResolver(builder).Visit(exp);
				return Expression.Quote(exp);
			}


			var delegat = arg as Delegate;
			if (delegat != null)
			{
			    var expr = delegat.ToExpression();
                return new ExpressionVarResolver(builder).Visit(expr.ReduceExtensions());
			}

    

			throw new Exception("Unexpected argument type: " + arg.GetType());
		}

	    object IInterfacedFactoryAccess.Access()
		{
			return this;
		}
	}

	internal class ExpressionVarResolver : ExpressionVisitor
	{
		private readonly ISyntaxTree builder;

		public ExpressionVarResolver(ISyntaxTree builder)
		{
			this.builder = builder;
		}

		protected override Expression VisitMethodCall(MethodCallExpression node)
		{
			if (node.Method.DeclaringType == typeof (IExtFactory))
			{
				// call like Ext.CreateXXX
			    var parameters = node.Method.GetParameters();
			    if (parameters.Length != node.Arguments.Count)
                    throw new Exception(string.Format("Expected {0} but {1}", parameters.Length, node.Arguments.Count));

                var value = new AstConstructor(node.Method.ReturnType)
                {
                    InsideEventHandler = true,
                    Args = node.Arguments.Zip(parameters, (expr, par) => new AstConstructorParameter
                    {
                        Name = par.Name,
                        Value = ReduceNullAsDefaults(par, new ExpressionVarResolver(builder).Visit(expr)),
                        AsJson = false
                    }).ToArray()

                };
				return value;
			}
			return base.VisitMethodCall(node);
		}

		private static Expression ReduceNullAsDefaults(ParameterInfo par, Expression expr)
		{
			if (par.HasDefaultValue && par.DefaultValue == null && expr.NodeType == ExpressionType.Convert)
			{
				var unaryOp = ((UnaryExpression) expr).Operand; 
				if (unaryOp.NodeType == ExpressionType.Constant && ((ConstantExpression)unaryOp).Value == null)
					return Expression.Default(par.ParameterType);
			}
			return expr;
		}

		protected override Expression VisitMember(MemberExpression node)
		{
			if (node.Expression.NodeType == ExpressionType.Constant)
			{
				// call like myTabs.Add()
				// we are accessing global js variable that asp.net has grounded to page class. 
				// We get the name from the expression and replace this node with reference
				var value = GetValue(node);
				var factoryObj = ((IInterfacedFactoryAccess) value).Access();
				if (factoryObj != null)
				{
                    var intfFactory = factoryObj as InterfacedFactory;
				    if (intfFactory != null)
				    {
				        var factory = builder.InterfaceFactories[intfFactory];
				        factory.PronouncedName = node.Member.Name;
				        return factory;
				    }
				    var extFactory = factoryObj as ExtFactoryProxy;
                    if (extFactory != null)
                    {
                        return new RootFactoryAccess(AstExpressionType.RootFactoryAccess, typeof (IExtFactory));
                    }
				}
			}
			return base.VisitMember(node);
		}

	    private static object GetValue(MemberExpression member)
		{
			var objectMember = Expression.Convert(member, typeof(object));
			var getterLambda = Expression.Lambda<Func<object>>(objectMember);

			var getter = getterLambda.Compile();

			return getter();
		}
	}

}